from symbide import ModuleManager, ThreadExecutionManager, OPTIONS, SHORTCUTS
from inputs import setInputType, INPUTS

from time import time

from xml.sax.saxutils import unescape
from xml.dom import minidom 

import gtk
from symbide.src.widgets.widgetOrganizer import WidgetOrganizer

#TODO: Enable Unicode support so that special characters can be inserted.
#TODO: Find out if parts of input and output can be merged

#if gtksourceview is not available, a gtk textview is used instead
__HAS_SOURCEVIEW__ = ModuleManager.hasModule('gtksourceview')

if __HAS_SOURCEVIEW__:
    import gtksourceview
    __TEXTVIEW__ = gtksourceview.SourceView
else:
    __TEXTVIEW__ = gtk.TextView
    

class Input(WidgetOrganizer, gtk.Frame):
    '''
    Just a gtk.Frame around the Input
    '''
    
    def __init__(self, ancestor, type='Python', DOM=None):
        if isinstance(ancestor, Input):
            label = None
        else:
            label = 'In'
        gtk.Frame.__init__(self, label)
        WidgetOrganizer.__init__(self, 'Input', ancestor)
        
        #the type determines what functions are available and if the commands get preparsed
        self.type = ''
        
        #Every Input in a worksheet gets a unique ID.
        self.ID = self.getAncestor('Worksheet').nextID()
        
        #other inputs holded by this input will be stored here after calling "refresh_subinputs"
        self.subinputs = []
        
        #here will be special output like errors or plots stored
        self._objects = []
        
        #the local namespace is shared throughout the whole worksheet
        self.local_namespace = self.getAncestor('Worksheet').namespace
        
        #the executor and the global namespace depend on the type of the Input
        self.global_namespace = {}
        self.executor = None
        
        self.time = 0
        
        #create TextView-widget
        self._createTextView(type, DOM)
    
    def _createTextView(self, type='Python', DOM=None):
        '''
        Creates a Textview and places it into the frame.
        '''
        
        if __HAS_SOURCEVIEW__:
            self.buffer = gtksourceview.SourceBuffer()
        else:
            self.buffer = gtk.TextBuffer()
        
        self.view = __TEXTVIEW__(self.buffer)
        self.view.show()
        self.add(self.view)
        tag = self.buffer.create_tag('global')
        
        #tag.set_property('size-points',17)
        tag.set_property('font', 'monospace regular 10')
        size = tag.get_property('size')
        rise = int(-3450-0.31*size)
        self._updateTags()
        tag = self.buffer.create_tag('subinput')
        tag.set_property('rise-set', True)
        tag.set_property('rise', rise)
        
        #event-handling
        self.view.connect('backspace', self._event_closeCell)
        self.view.connect('key_press_event', self._event_keyPress)
        self.view.connect('move-cursor', self._event_moveCursor)
        self.view.connect('focus-in-event', self._event_focusIn)
        self.view.connect('populate-popup', self._event_popup)
        self.view.connect('event-after', self._event_after)
        self.buffer.connect('changed', self._event_changed)
        
        #sets the style of the textview
        self.view.set_border_window_size(gtk.TEXT_WINDOW_RIGHT, 1)
        self.view.set_border_window_size(gtk.TEXT_WINDOW_LEFT, 1)
        self.view.set_border_window_size(gtk.TEXT_WINDOW_TOP, 1)
        self.view.set_border_window_size(gtk.TEXT_WINDOW_BOTTOM, 1)
        
        #TODO: let the styles be set by the user
        
        if __HAS_SOURCEVIEW__:
            manager = gtksourceview.SourceLanguagesManager()
            language = manager.get_language_from_mime_type("text/x-python")
                        
            self.buffer.set_language(language)
            self.buffer.set_highlight(True)
            
            #self.view.set_show_line_numbers(True)
            self.view.set_smart_home_end(True)
            #self.view.set_show_margin(True)
            self.view.set_tabs_width(int(OPTIONS['tabswidth']))
            self.view.set_auto_indent(True)
            
        if DOM:
            self.setdom(DOM)
        else:
            self.setType(type)
    
    def _event_focusIn(self, widget, *args):
        self.getAncestor('Notebook').activeInput = self
        self.getAncestor('Notebook').set_cursor_moved()
    
    def _event_changed(self, buffer):
        '''
        Update the styling tags in buffer, and set self.changed True.
        '''
        
        self._updateTags()
        self.changed = True
    
    def _event_after(self, widget, event):
        if event.type is gtk.gdk.KEY_PRESS:
            self.scroll_to()
    
    def _event_popup(self, widget, popup):
        menuitem = gtk.MenuItem('Change Input-type to ..')
        menuitem.show()
        submenu = gtk.Menu()
        submenu.show()
        menuitem.set_submenu(submenu)
        keys = INPUTS.keys()
        keys.sort()
        for i in keys:
            if i==self.type:
                continue
            item = gtk.MenuItem(i)
            item.show()
            item.connect('activate', lambda t, i=i: self.setType(i))
            submenu.append(item)
        popup.prepend(menuitem)
        
        menuitem = gtk.MenuItem('Open Input as Pythonsheet')
        menuitem.connect('activate', lambda t:self.getAncestor('Notebook').addPythonsheet(self.totext()))
        menuitem.show()
        popup.prepend(menuitem)
    
    def _event_keyPress(self, widget, event):
        '''
        Reacts to shortcuts for evaluateCell, undo, redo.
        '''
        
        key = event.keyval
        masks = gtk.accelerator_get_default_mod_mask()
        
        events = (('executeCell', 'execute'),
                  ('undo', 'undo'),
                  ('redo', 'redo'),
                  )
                  
        for i, j in events:
            keyval, modifier = SHORTCUTS[i]
            if key==keyval and masks & event.state==modifier:
                getattr(self, j)()
                return True
        
        events = (('insertSympyInput', 'Sympy'),
                  ('insertPythonInput', 'Python'),
                  ('insertTextInput', 'Text'),
                  )
        
        for i, j in events:
            keyval, modifier = SHORTCUTS[i]
            if key==keyval and masks & event.state==modifier:
                getattr(self, 'insertInput')(j)
                return True
            
        events = (('executeWorksheet', 'execute_current_sheet'),
                  ('openWorksheet', 'openSheet'),
                  ('saveWorksheet', 'saveSheet'),
                  ('saveWorksheetAs', 'saveSheetAs'))
        
        for i, j in events:
            keyval, modifier = SHORTCUTS[i]
            if key==keyval and masks & event.state==modifier:
                getattr(self.getAncestor('Notebook'), j)()
                return True
            
        keyval, modifier = SHORTCUTS['find']
        if key==keyval and masks & event.state==modifier:
            find = self.getAncestor('MainWindow').getChildren('Find')
            if self.buffer.get_has_selection():
                start, end = self.buffer.get_selection_bounds()
                text = self.buffer.get_text(start, end, include_hidden_chars=False)
                find.entry.set_text(text.split('\n', 2)[0])
            find.entry.grab_focus()
            find.show()
         
        if masks & event.state==gtk.gdk.SHIFT_MASK:
            return
           
        events = ((65364, '_cursor_down'), 
                  (65362, '_cursor_up'),
                  (65361, '_cursor_left'),
                  (65363, '_cursor_right'),
                  )
        
        for keyval, callback in events:
            if key==keyval:
                return getattr(self, callback)()
    
    def _event_moveCursor(self, textview, step_size, count, extend_selection):
        '''
        Ensures that the cursor is always visible.
        '''
        
        self.getAncestor('Notebook').cursor_moved = True
    
    def _event_closeCell(self, widget):
        '''
        Closes the cell if the following is True:
            -Input is empty
            -Double-backspace-click in a defined period
        If it's a subinput not the whole cell is closed.
        '''
            
        #only react to double backspace:
        t = time()
        dt = t-self.time
        if dt<0.1 or dt>0.4:
            self.time = t
            return False
        
        #don't close if input-cell not empty
        if self.buffer.get_char_count()!=0:
            return False
        
        #if it's a subinput
        if isinstance(self.ancestor, Input):
            for s in self.ancestor.subinputs:
                if s[0] is self:
                    start = self.ancestor.buffer.get_iter_at_child_anchor(s[1])
                    end = start.copy()
                    end.forward_cursor_position()
                    self.ancestor.buffer.delete(start, end)
                    self.ancestor.buffer.place_cursor(end)
            return True
        
        #remove cell and appendant cellAdder from WorksheetVBox
        self.getAncestor('Worksheet').closeCell(self.ancestor)
        return True
    
    def _cursor_down(self):
        cursorIter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
        #jump to next input if at last line
        if cursorIter.get_line() == self.buffer.get_line_count()-1:
            if self.ancestor.identifier == 'Input':
                self.ancestor.placeCursor(self, 0, 1)
            else:
                self.getAncestor('Worksheet').focusAt(self.getAncestor('Cell'), 1, 0)
            return True
        #enter subinput if there is one at this place
        cursorIter.forward_visible_line()
        return self._enterSubinputAtIter(cursorIter, 0)
    
    def _cursor_up(self):
        cursorIter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
        #jump to last input if at first line
        if cursorIter.get_line() == 0:
            if self.ancestor.identifier == 'Input':
                self.ancestor.placeCursor(self, 0, -1)
            else:
                self.getAncestor('Worksheet').focusAt(self.getAncestor('Cell'), -1, -1)
            return True
        #enter subinput if there is one at this place
        cursorIter.backward_visible_line()
        return self._enterSubinputAtIter(cursorIter, -1)
    
    def _cursor_left(self):
        cursorIter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
        #jump to last input if at first line
        if cursorIter.is_start():
            if self.ancestor.identifier == 'Input':
                self.ancestor.placeCursor(self, 0, 0)
            else:
                self.getAncestor('Worksheet').focusAt(self.getAncestor('Cell'), -1, -1)
            return True
        cursorIter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
        #enter subinput if there is one at this place
        cursorIter.backward_cursor_position()
        return self._enterSubinputAtIter(cursorIter, -1)
        
    def _cursor_right(self):
        cursorIter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
        #jump to last input if at last position
        if cursorIter.is_end():
            if self.ancestor.identifier == 'Input':
                self.ancestor.placeCursor(self, 1, 0)
            else:
                self.getAncestor('Worksheet').focusAt(self.getAncestor('Cell'), 1, 0)
            return True
        cursorIter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
        #enter subinput if there is one at this place
        return self._enterSubinputAtIter(cursorIter, 0)
        
    def _enterSubinputAtIter(self, iter, cursorPosition=0):
        anchor = iter.get_child_anchor()
        if anchor is None:
            return
        for a in anchor.get_widgets():
            if isinstance(a, Input):
                a.placeCursor(charPos=cursorPosition)
                return True
    
    def _updateTags(self):
        '''
        Update the styling tags in buffer.
        '''
        
        start, end = self.buffer.get_bounds()
        self.buffer.apply_tag_by_name('global', start, end)
    
    def insertInput(self, type, place=None, DOM=None):
        '''
        Inserts an Input of type=type at the cursor. If a iter is given for
        place, it is inserted there.
        '''
        
        if place is None:
            insert = self.buffer.get_insert()
            place = self.buffer.get_iter_at_mark(insert)
        anchor = self.buffer.create_child_anchor(place)
        end, start = place.copy(), place.copy()
        start.backward_cursor_position()
        self.buffer.apply_tag_by_name('subinput', start, end)
        self.subinputs.append((Input(self, type=type, DOM=DOM), anchor))
        self.addChild(self.subinputs[-1][0])
        self.view.add_child_at_anchor(self.subinputs[-1][0], anchor)
    
    def setType(self, type):
        if not type:
            type = 'Python'
        self.type = type
        setInputType(self)
        
    def placeCursor(self, neighbor=None, charPos=0, linePos=0, focus=True):
        '''
        Places the cursor relative to neighbor, shifted by char_pos chars and
        line_pos lines. If neighbor is not given it starts either relative to
        the end or relative to the beginning. 
        '''
        
        if isinstance(neighbor, Input):
            for s in self.subinputs:
                if s[0] is neighbor:
                    iter = self.buffer.get_iter_at_child_anchor(s[1])
                    break
        elif isinstance(neighbor, gtk.TextIter):
            iter = neighbor
        elif neighbor is None:
            if charPos<0:
                iter = self.buffer.get_end_iter()
                charPos = charPos+1
            elif linePos<0:
                iter = self.buffer.get_end_iter()
            else:
                iter = self.buffer.get_start_iter()

        iter.forward_lines(linePos)
        iter.forward_chars(charPos)
        self.buffer.place_cursor(iter)
        self.scroll_to()
        if focus:
            self.view.grab_focus()
    
    def scroll_to(self, pos=None):
        if isinstance(pos, gtk.TextIter):
            iter = pos
        elif isinstance(pos, gtk.TextMark):
            iter = self.buffer.get_iter_at_mark(pos)
        else:
            iter = self.buffer.get_iter_at_mark(self.buffer.get_insert())
        
        worksheet = self.getAncestor('Worksheet')
        if worksheet.allocation.height==1:
            return

        #buffer coordinates of iter
        location = self.view.get_iter_location(iter)
        
        #textview coordinates of iter
        x, y = self.view.buffer_to_window_coords(gtk.TEXT_WINDOW_WIDGET, location.x, location.y)
        
        #worksheet coordinates of iter
        x, y = self.view.translate_coordinates(worksheet, x, y)
        
        #set vadjustment
        if y<10:
            vadjustment = worksheet.get_vadjustment()
            vadjustment.set_value(vadjustment.value+y-10)
        elif y>worksheet.allocation.height-location.height-30:
            vadjustment = worksheet.get_vadjustment()
            vadjustment.set_value(vadjustment.value+y-worksheet.allocation.height+40)
        
        #set hadjustment
        if x<10:
            hadjustment = worksheet.get_hadjustment()
            hadjustment.set_value(hadjustment.value+x-20)
        elif x>worksheet.allocation.width-30:
            hadjustment = worksheet.get_hadjustment()
            hadjustment.set_value(hadjustment.value+x-worksheet.allocation.width+30)
        
    def get_search_area(self, start_iter, forward):
        subinput = self.get_next_subinput(start_iter, forward)
        if subinput:
            end_iter = self.buffer.get_iter_at_child_anchor(subinput[1])
            next_input = subinput[0]
            if forward:
                next_start_iter = next_input.buffer.get_start_iter()
            else:
                next_start_iter = next_input.buffer.get_end_iter()
        else:
            if forward:
                end_iter = self.buffer.get_end_iter()
            else:
                end_iter = self.buffer.get_start_iter()
            next_input, next_start_iter = self.get_next_outer_input(forward)
        return (start_iter, end_iter), (next_input, next_start_iter)
        
    def get_next_subinput(self, start_iter, forward):
        self.sortSubinputs()
        if forward:
            comparison = (-1, )
        else:
            comparison = (1, 0, )
            self.subinputs.reverse()
        for s in self.subinputs:
            iter = self.buffer.get_iter_at_child_anchor(s[1])
            if not iter.compare(start_iter) in comparison:
                return s
            
    def get_next_outer_input(self, forward):
        if isinstance(self.ancestor, Input):
            self.ancestor.sortSubinputs()
            for s in self.ancestor.subinputs:
                if s[0] is self:
                    iter = self.ancestor.buffer.get_iter_at_child_anchor(s[1]).copy()
                    if forward:
                        iter.forward_char()
                    return self.ancestor, iter
            raise
        elif self.ancestor.identifier=='Cell':
            cells = self.getAncestor('Worksheet').children
            pos = cells.index(self.ancestor)
            if forward:
                nextPos = pos+2
                if nextPos>len(cells)-1:
                    nextPos = 1
            else:
                nextPos = pos-2
                if nextPos<0:
                    nextPos = len(cells)-2
            
            next_input = cells[nextPos].getChildren('Input')
            
            if forward:
                iter = next_input.buffer.get_start_iter()
            else:
                iter = next_input.buffer.get_end_iter()
            return next_input, iter
        else:
            if forward:
                return self, self.buffer.get_start_iter()
            else:
                return self, self.buffer.get_end_iter()
        
    def undo(self):
        '''
        Undos the last user action in buffer. Only possible if
        gtksourceview is available.
        '''
        
        if __HAS_SOURCEVIEW__ and self.buffer.can_undo():
            self.buffer.undo()
        
    def redo(self):
        '''
        Redos the last user action in buffer. Only possible if
        gtksourceview is available.
        '''
        
        if __HAS_SOURCEVIEW__ and self.buffer.can_redo():
            self.buffer.redo()
    
    def sortSubinputs(self):
        '''
        Sorts the subinputs-list according to their appearance in the buffer and
        removes subinputs that are deleted.
        '''
        
        buf = []
        for s in self.subinputs:
            if not s[1].get_deleted():
                buf.append(s)
        self.subinputs = buf
        
        def sort(a, b):
            a_iter = self.buffer.get_iter_at_child_anchor(a[1])
            b_iter = self.buffer.get_iter_at_child_anchor(b[1])
            return a_iter.compare(b_iter)
        self.subinputs.sort(sort)
    
    def totext(self):
        '''
        Returns the text in the buffer.
        '''
        
        return self.buffer.get_text(*self.buffer.get_bounds())
    
    def todom(self, *args):
        '''
        Returns a DOM representation of Input.
        '''
        
        DOM = minidom.Element('input')
        DOM.setAttribute('type', self.type)
        
        self.sortSubinputs()
        last_iter = self.buffer.get_start_iter()
        for s in self.subinputs:
            iter = self.buffer.get_iter_at_child_anchor(s[1])
            text = self.buffer.get_text(last_iter, iter)
            DOM_text = minidom.Document().createTextNode(text)
            DOM.appendChild(DOM_text)
            DOM.appendChild(s[0].todom())
            last_iter = iter
            last_iter.forward_char()
            
        text = self.buffer.get_text(last_iter, self.buffer.get_end_iter())
        DOM_text = minidom.Document().createTextNode(text)
        DOM.appendChild(DOM_text)
        return DOM 
    
    def setdom(self, DOM):
        '''
        Replaces the current Input with the Input represented in the DOM.
        '''
        
        if DOM.hasAttribute('type'):
            self.setType(DOM.getAttribute('type'))
        else:
            self.setType('Python')
        
        if not DOM.childNodes:
            return
        self.buffer.set_text('')
        
        #some shortcuts
        end = self.buffer.get_end_iter
        append = lambda text:self.buffer.insert(end(), text)
        
        for child in DOM.childNodes:
            if child.nodeType is child.TEXT_NODE:
                append(unescape(child.data))
            elif child.nodeType is child.ELEMENT_NODE:
                if child.hasAttribute('type'):
                    type = child.getAttribute('type')
                else:
                    type = 'Python'
                self.insertInput(type, end(), child)
    
    def getCommands(self):
        '''
        Returns the code of the input, with all other embedded inputs replaced
        by __evaluate_input__(number). 
        '''
        
        self.sortSubinputs()
        commands = []
        last_iter = self.buffer.get_start_iter()
        for a in self.subinputs:
            iter = self.buffer.get_iter_at_child_anchor(a[1])
            commands.append(self.buffer.get_text(last_iter, iter))
            last_iter = iter
            last_iter.forward_char()
        commands.append(self.buffer.get_text(last_iter, self.buffer.get_end_iter()))
        return commands
        
    def execute(self):
        '''
        Executes the main Input.
        '''
        
        if isinstance(self.ancestor, Input):
            self.ancestor.execute()
        else:
            self.__execute__()
            
    def __execute__(self):
        '''
        Executes the text in the Input buffer in the execution thread.
        '''
        
        self.getAncestor('Cell').call('Output', 'clear')
        self.getAncestor('Cell').call('Output', 'wait')
        ThreadExecutionManager.add_job(self.executor.execute, self.type, self)
            
    def __execute_instantly__(self):
        '''
        Executes the text in the Input buffer. This should be called only
        out of the execution thread when executing subinputs.
        '''
        
        return self.executor.execute(self, main=False)
    